<?php
/**
 * @package qBase
 * @subpackage web
 */


/**
 * QWebPage is the base for any web page
 * 
 * @package qBase
 * @subpackage web
 */
class QWebPage extends QWebControl implements QIWebListener 
{
	/**
	 * This ajax chunk type evaluates a string in javascript with eval. The data is just a string.
	 *
	 */
	const AjaxChunkTypeExecute = 1;
	/**
	 * This ajax chunk type replaces a DOM element with a string. The first parameter is a string representing the id of the element and the second is the HTML to use inplace of the element.
	 *
	 */
	const AjaxChunkTypeReplace = 2;
	/**
	 * The page that is currrently running
	 *
	 * @var QWebPage
	 */
	private static $_LoadedPage;
	/**
	 * The request that the page is processing
	 *
	 * @var QWebRequest
	 */
	private $_request;
	/**
	 * The request that the page is processing
	 *
	 * @var QWebResponse
	 */
	private $_response;
	/**
	 * The base href of the page
	 *
	 * @var string
	 */
	private $_base_href = null;
	/**
	 * The request URL of the page , relative to base href
	 *
	 * @var string
	 */
	private $_request_url;
	/**
	 * The title of the web page
	 *
	 * @var string
	 */
	private $_title = null;
	/**
	 * The description of the web page
	 *
	 * @var string
	 */
	private $_description = null;
	/**
	 * The keywords of the web page
	 *
	 * @var string
	 */
	private $_keywords = null;
	/**
	 * The javascript stream
	 *
	 * @var string
	 */
	private $_js_stream = "";
	/**
	 * The doctype of the web page
	 *
	 * @var string
	 */
	private $_doc_type = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
	/**
	 * The extra html to be added in the <html> tag
	 * ex: if $this->_html_extra = 'xmlns="http://www.w3.org/1999/xhtml"' => <html xmlns="http://www.w3.org/1999/xhtml">
	 *
	 * @var unknown_type
	 */
	private $_html_extra = "";
	/**
	 * If true the basic javascript files will be included. The files are part of the framework and should be included.
	 * Not including the default javascript files will break parts of the functionality.
	 *
	 * @var boolean
	 */
	private $_include_default_js = true;
	/**
	 * If true the basic css files will be included. The files are part of the framework and should be included but unlike the javascript files will not break parts of the functionality.
	 *
	 * @var boolean
	 */
	private $_include_default_css = true;
	/**
	 * The list of javascript objects equivalents for all the web controls used. This array is created automaticaly.
	 * Ex: QWebControl (php) => QWebControl (javascript)
	 *
	 * @var array
	 */
	private $_js_controls_equivalents = array();
	/**
	 * The list with the location of the javascript files that contains the objects that are the equivalents for all the web controls used.
	 *
	 * @var array
	 */
	private $_js_controls_equivalents_paths = array();
	/**
	 * The QIWebListener that called the page
	 *
	 * @var QIWebListener
	 */
	private $_parent_web_listener;
	/**
	 * True if it's a ajax request
	 *
	 * @var boolean
	 */
	private $_is_ajax;
	/**
	 * The id of the instance of the page. Based on the Id the page knows what viewstate to load.
	 *
	 * @var string
	 */
	private $_instance_id;
	/**
	 * True if it's a postback or callback request
	 *
	 * @var boolean
	 */
	private $_is_postback = false;
	
	/**
	 * The chunks to be sent to ajax, see the function that adds the chunk for more details
	 *
	 * @var array
	 */
	private $_ajax_chunks = array();
	/**
	 * The current position of the chunks. Keys for chunks must not be reinserted.
	 *
	 * @var integer
	 */
	private $_ajax_pos = 0;
	/**
	 * A list with all the resources to be saved to the viewstate
	 * The list contains all the resources including the resources 
	 * of the child controls (full tree)
	 *
	 * @var array
	 */
	private $_all_resources = array();
	/**
	 * If an event is triggered we must call onAfterEvent on the control
	 * in this variable we place the JS that will execute "onAfterEvent"
	 *
	 * @var array
	 */
	private $_event_callback_js_data;
	/**
	 * The events that will be triggered
	 *
	 * @var array
	 */
	private $_trigger_events = null;
	
	private $_has_triggered_events = false;
	
	private $_session_load_time = 0;
	
	/**
	 * The constructor of the web page
	 *
	 */
	public function __construct()
	{
		parent::__construct();
	}
	
	/**
	 * Gets the javascript class equivalent for the specified class name
	 *
	 * @param string $class_name
	 * @return string
	 */
	protected final function getJsClassEquiv($class_name)
	{
		if (!isset($this->_js_controls_equivalents[$class_name]))
			$this->getClassJsNeededFiles($class_name);
			
		return $this->_js_controls_equivalents[$class_name];
	}
	
	/**
	 * Gets the needed javascript files for the specified class name
	 *
	 * @param string $class_name
	 * @return string
	 */
	protected final function getClassJsNeededFiles($class_name = null)
	{
		if (!$class_name)
			$class_name = $this->getClassName();

		if (isset($this->_js_controls_equivalents_paths[$class_name]))
			return $class_name;

		// $o_class_name = $class_name;
		$first = null;
		
		// we should put things in reverse order
		$class_stack = array();
		while ($class_name)
		{
			$class_stack[] = $class_name;
			if (strtolower($class_name) == "qwebcontrol")
				break;
			$class_name = get_parent_class($class_name);
		}
				
		$class_stack = array_reverse($class_stack);
		
		$prev_file = null;
		$prev_class = null;
		
		foreach ($class_stack as $class)
		{
			if (isset($this->_js_controls_equivalents[$class]))
			{
				$prev_class = $this->_js_controls_equivalents[$class];
				$prev_file = $this->_js_controls_equivalents_paths[$class];
				continue;
			}
			// var_dump("js/{$class_name}.js", $class_name);

			$file = QWebApp::RelativePath( QAutoload::GetClassPath($class) ); // $this->res("js/{$class}.js", $class, false);
			$file = substr($file, 0, -4) . ".js";
			if (!file_exists($file))
				$file = null;
				
			if ($file)
			{
				$prev_class = $class;
				$prev_file = $file;
			}

			// at least the base class must have a JS object
			if (!$prev_class)
				throw new Exception("At least the base class must have an equivalent JS class");

			if ((!$file) && $prev_file)
				$file = $prev_file;
			
			if ($file)
			{
				$this->_js_controls_equivalents[$class] = $prev_class;
				$this->_js_controls_equivalents_paths[$class] = $file;
			}
		}
		
		return $prev_class;
	}
	
	/**
	 * Gets the web module that executes this web page
	 *
	 * @return QWebModule
	 */
	public final function getModule()
	{
		if (!$this->_parent_web_listener)
			return null;
		else if ($this->_parent_web_listener instanceof QWebModule)
			return $this->_parent_web_listener;
		else 
			return null;
	}

	/**
	 * Processes the web request
	 *
	 * @param QWebRequest $request
	 * @param QIWebListener $parent
	 */
	public final function processRequest(QWebRequest $request, QIWebListener $parent = null)
	{
		self::$_LoadedPage = $this;
		
		if ($parent)
			$this->_parent_web_listener = $parent;
		
		if ($request->get("__qid"))
		{
			$this->_is_postback = true;
			$this->_instance_id = $request->get("__qid");
		}
		else 
		{
			$this->_instance_id = uniqid("__vs");
		}

		if ($request->get("__qajax") == "1")
		{
			$this->_is_ajax = true;
		}
		
		if ((!$this->_base_href) && (($parent instanceof QWebApp) || ($parent instanceof QWebModule) || 
			($parent instanceof QWebPage)))
		{
			$this->_base_href = $parent->getBaseHref();
		}
		if ((!$this->_request_url) && (($parent instanceof QWebApp) || ($parent instanceof QWebModule) || 
			($parent instanceof QWebPage)))
		{
			$this->_request_url = $parent->getRequestUrl();
		}
		
		$this->_request = $request;
		$this->_response = $request->Response();

		// QBaseInit::PrintElapsedTime(4, true);
		$s_1 = microtime(true);
		QWebContext::Session()->startOrResume();
		$this->_session_load_time = microtime(true) - $s_1;
		if ($this->_session_load_time >= 0.01)
			$this->debug("Warning: Session load time = " . round($this->_session_load_time, 5) . " sec");

		if ($this->getUrlManager())
			$this->getUrlManager()->load($this->getCallStack(true));

		if ($this->isPostback())
			$this->_viewstate = QWebContext::Session()->getVariable($this->_instance_id);

		$this->decodeRequestEvents();
		
		
		/** THE OLD WAY
		// call init on this and all controls that will attach to this page
		$this->doInit();
		
		// TriggerAfterInit
		$this->triggerRequestEvents(QWebEvent::TriggerAfterInit);
		
		if ($this->isPostback())
		{
			// load viewstate
			$this->doLoadViewstate($this);
			// load postback data
			$this->doLoadPostback($this);
		}
		
		// TriggerBeforeLoad
		$this->triggerRequestEvents(QWebEvent::TriggerBeforeLoad);
		// call onload event
		$this->doLoad($this);

		// trigger events TriggerAfterLoad
		$this->triggerRequestEvents(QWebEvent::TriggerAfterLoad);

		$this->_has_triggered_events = true;
		$this->doAfterEvents($this);
		
		*/
		
		// The new way
		$this->doLoadControl($this, true, true, true, true, true, true);
		
		// call for render
		$this->renderPage();
				
		// save viewstate
		$this->doSaveViewstate($this);
		
		if ($this->getUrlManager())
			$this->getUrlManager()->unload();
		$this->_viewstate["__all_resources"] = $this->_all_resources;
		
		// now put the viewstate in session
		QWebContext::Session()->setVariable($this->_instance_id, $this->_viewstate);

		// call unonload
		$this->doUnload($this);
	}
	
	public final function getViewstateData(QWebControl $ctrl, $key = null)
	{
		if (!$this->_viewstate)
			return null;
			
		if ($ctrl === $this)
		{
			if ($key)
			{
				if (isset($this->_viewstate[$key]))
					return $this->_viewstate[$key];
				else 
					return null;
			}
			else 
				return $this->_viewstate;
		}
			
		$c = $ctrl;
		$stack = array();
		while (true)
		{
			$stack[] = $c;
			$c = $c->getParent();
			if ($c->isWebPage())
				break;
			if (!$c)
				throw new Exception("getViewsatateData can not find the controls's root page");
		}
		$vs = &$this->_viewstate;
		for ($i = (count($stack) - 1); $i >= 0; $i--)
		{
			$c_id = $stack[$i]->getId();
			if (isset($vs["_ctrls"][$c_id]))
				$vs = &$vs["_ctrls"][$c_id];
			else 
				return null;
		}
		
		if ($key)
		{
			if (isset($vs[$key]))
				return $vs[$key];
			else 
				return null;
		}
		else 
			return $vs;
	}
	
	/**
	 * The request that the page is processing
	 *
	 * @return QWebRequest
	 */
	public final function request()
	{
		return $this->_request;
	}
	
	/**
	 * The QWebResponse associated to the page
	 *
	 * @return QWebResponse
	 */
	public final function response()
	{
		return $this->_response;
	}
	
	/**
	 * Appends a string to the current web response
	 *
	 * @param string $string
	 */
	public final function output($string)
	{
		$this->_response->append($string);
	}
	
	/**
	 * Includes a css file in the response
	 *
	 * @param string $file
	 */
	private final function insertCss($file, $attrs = null)
	{
		$this->output("<link rel=\"stylesheet\" type=\"text/css\" href=\"{$file}\" />\n");
	}
	
	/**
	 * Includes a javascript file in the web response
	 *
	 * @param string $file
	 */
	private final function insertJs($file, $attrs = null)
	{
		$this->output("<script src=\"{$file}\" type=\"text/javascript\"><!-- --></script>\n");
	}

	private final function insertResource($path, $type, $attrs)
	{
		if ($type == "js")
			$this->insertJs($path, $attrs);
		else if ($type == "css")
			$this->insertCss($path, $attrs);
		else 
			throw new Exception("Unknown resource type [{$type}]");
	}
	
	private final function prepareTitleDescriptionKeywordsForRender()
	{
		$new_title = $this->getViewstate("new_page_title");
		if ($new_title)
		{
			$this->setViewstate("page_title", $new_title);
			$this->unsetViewstate("new_page_title");
			if ($this->isAjaxRequest())
				$this->setJsParam("page_title", $new_title);
		}
		$new_description = $this->getViewstate("new_page_description");
		if ($new_description)
		{
			$this->setViewstate("page_description", $new_description);
			$this->unsetViewstate("new_page_description");
			if ($this->isAjaxRequest())
				$this->setJsParam("page_description", $new_description);
		}
		$new_keywords = $this->getViewstate("new_page_keywords");
		if ($new_keywords)
		{
			$this->setViewstate("page_keywords", $new_keywords);
			$this->unsetViewstate("new_page_keywords");
			if ($this->isAjaxRequest())
				$this->setJsParam("page_keywords", $new_keywords);
		}
	}
	
	/**
	 * Renders the page and build the javascript stream
	 *
	 */
	private final function renderPage()
	{
		QWebApp::$ClenupNeeded = true;
		
		// first call the render then the rest
		// the idea is to trigger any other resources if controls are created at render
		$out = null;
		if ($this->isAjaxRequest())
		{
			$this->renderAjax($this, $this->getFullId());
		}
		else 
		{
			ob_start();
			$this->render($this);
			$out = ob_get_clean();
		}
		
		// title & description & keywords
		$this->prepareTitleDescriptionKeywordsForRender();
		
		$this->prepareRender($this, $this->getFullId());
		
		// set the needed JS files
		if ($this->_js_controls_equivalents_paths)
			foreach ($this->_js_controls_equivalents_paths as $eq_class => $js)
			{
				$this->addJs($js, array("isQWebControl" => true, 
					"qClassName" => $this->_js_controls_equivalents[$eq_class]), false);
			}

		$this->addControlResources($this->getResourcesData());

		if ($this->isAjaxRequest())
		{
			// $this->renderAjax($this, $this->getFullId());
			
			// determine new resource files that need to be added 
			$all_res_keys = array_keys($this->_all_resources);
			$old_res_ref = &$this->_viewstate["__all_resources"];
			if ($old_res_ref)
				$old_res_files = array_keys($old_res_ref);
			else 
				$old_res_files = array();
			// files to be added
			$diff_res_files = array_diff($all_res_keys, $old_res_files);
			// files to be removed
			$remove_res_files = array_diff($old_res_files, $all_res_keys);

			// the new JS classes that will be added . The JS client will have to wait until they are loaded and only after can execute the rest of the code
			$new_js_classes = array();
			// add new files
			if ($diff_res_files && (count($diff_res_files) > 0))
				foreach ($diff_res_files as $res_file)
				{
					$res_file_data = $this->_all_resources[$res_file];

					if (($res_file_data["type"] == "js") && (isset($res_file_data["attrs"]["isQWebControl"])) && 
						$res_file_data["attrs"]["isQWebControl"])
						$new_js_classes[] = $res_file_data["attrs"]["qClassName"];
						
					$this->output("GetQWebPage().addResourceDynamically(".
						json_encode($res_file).", ".
						json_encode($res_file_data["type"]).", ".
						json_encode($res_file_data["attrs"]).");\n");
				}
				
			// $this->addAjaxExecuteChunk("alert(".json_encode($new_js_classes).");");

			// remove old files except JS files
			if ($remove_res_files && (count($remove_res_files) > 0))
				foreach ($remove_res_files as $res_file)
				{
					$res_file_data = $old_res_ref[$res_file];
					
					if ($res_file_data["type"] == "js")
						$this->_all_resources[$res_file] = $res_file_data;
					else 
					{
						$this->output("GetQWebPage().removeResourceDynamically(".
							json_encode($res_file).", ".
							json_encode($res_file_data["type"]).");\n");
					}
				}

			if (strlen($this->_js_stream) > 0)
			{
				$this->addAjaxExecuteChunk($this->_js_stream);
			}

			$new_stream = "";

			foreach ($this->_ajax_chunks as $chunk)
			{
				$type = $chunk[0];
				$param = $chunk[1];
				
				switch ($type)
				{
					case QWebPage::AjaxChunkTypeExecute:
						{
							$new_stream .= $param;// $this->output($param);
							break;
						}
					case QWebPage::AjaxChunkTypeReplace:
						{
							// run replace chunks after 
							$this->output("qjs_" . $this->getFullId() . ".replaceDomElement(".
								json_encode($param[0]).", ".json_encode($param[1]).");\n");
							break;
						}
					default:
						{
							throw new Exception("QWebPage::renderPage() unknown ajax chunk type : {$type}");
							break;
						}
				}
			}
			
			if (strlen($new_stream) > 0)
			{
				$js_save_name = uniqid() . ".js";
				$js_save_path = QWebApp::GetTempPath() . "js_streams/" . $js_save_name;
				$js_rel_path = QWebApp::GetRelativeTempPath() . "js_streams/" . $js_save_name;
				
				file_put_contents($js_save_path, $new_stream);
				
				
				$this->output("GetQWebPage().addJsDynamically(".
						json_encode($js_rel_path).", ".
						json_encode(null).", ".
						json_encode($new_js_classes).");\n");
			}
			
			if ($this->getUrlManager())
			{
				$hash = $this->getUrlManager()->getHash();
				if ($this->getUrlManager()->getUpdateHash())
				{
					// $this->debug("Update hash !");
					
					$this->output("GetQWebPage().setHashInBrowser(".json_encode($hash).");\n");
				}
			}
			
			$this->outputDebugData();
		}
		else 
		{
			$this->output("{$this->_doc_type}\n<html{$this->_html_extra}>\n<head>\n");

			if (!$this->_title) // must have a title in IE or page does not render
				$this->_title = " ";
				
			$this->output("<title>".htmlspecialchars($this->_title)."</title>\n");
			if (isset($this->_description) && $this->_description)
				$this->output("<meta name=\"description\" content=\"".htmlspecialchars($this->_description)."\" />\n");
			if (isset($this->_keywords) && $this->_keywords)
				$this->output("<meta name=\"keywords\" content=\"".htmlspecialchars($this->_keywords)."\" />\n");

			if (!is_null($this->getBaseHref()))
				$this->output("<base href=\"{$this->getUrlBaseHref()}\" />\n");

			if ($this->_include_default_css)
			{
				$this->insertCss(QWebApp::FwRel("res/misc/css/normalize.css"));
			}
			
			if ($this->_include_default_js)
			{
				$this->insertJs(QWebApp::FwRel("res/misc/js/jquery-1.4.1.min.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/jquery.copy.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/jquery.dimensions.js"));
				/* $this->insertJs(QWebApp::FwRel("res/misc/js/jquery.hashchange-1.0.0.js")); */
				$this->insertJs(QWebApp::FwRel("res/misc/js/json2.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/jquery.history.js"));
				// $this->insertJs(QWebApp::FwRel("res/misc/js/jquery.debug.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/jquery.bgiframe.min.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/functions.js"));
				$this->insertJs(QWebApp::FwRel("res/misc/js/QObject.js"));
			}

			if ($this->_all_resources)
			{
				foreach ($this->_all_resources as $path => $data)
					$this->insertResource($path, $data["type"], $data["attrs"]);
			}
			
			// $this->_js_stream = "alert('aaa');";
			// js stream
			if (strlen($this->_js_stream) > 0)
			{
				$js_save_name = uniqid() . ".js";
				$js_save_dir = QWebApp::GetTempPath() . "js_streams/" ;
				$js_save_path = $js_save_dir . $js_save_name;
				$js_rel_path = QWebApp::GetRelativeTempPath() . "js_streams/" . $js_save_name;
				
				if (!is_dir($js_save_dir))
					mkdir($js_save_dir, 0755);
				file_put_contents($js_save_path, $this->_js_stream);
				
				$this->insertJs($js_rel_path);
			}
	
			$this->output("</head>\n<body>\n");
			// render the page
			// populate the response
 			$this->output($out);
			
			$this->outputDebugData();
			
			$this->output("<div id='___qWebAjaxLoader' style='display: none;'><img src='".$this->res("ajax-loader.gif")."' /><div>Loading</div></div>");
			
			$this->output("\n</body>\n</html>\n");
		}
	}
	
	public final function addTriggerEvent($sender_id, $type, $params)
	{
		if (!$this->_trigger_events)
			$this->_trigger_events = array();
		$this->_trigger_events[] = array("id" => $sender_id, "type" => $type, "params" => $params);
	}
	
	public final function hasTriggeredEvents()
	{
		return $this->_has_triggered_events;
	}
	
	public final function countTriggerEvents()
	{
		if (!$this->_trigger_events)
			return 0;
		else 
			return count($this->_trigger_events);
	}
	
	public final function getTriggerEventAt($pos)
	{
		if ((!$this->_trigger_events) || (!isset($this->_trigger_events[$pos])))
			return null;
		else
			return $this->_trigger_events[$pos];
	}

	public final function getTriggerEvent($sender_id, $event_type)
	{
		if (!$this->_trigger_events)
			return null;
		else
		{
			foreach ($this->_trigger_events as $ev)
			{
				if ((($ev["id"] == $sender_id) && ($ev["type"] == $event_type)) || 
					(($sender_id == $this->getFullId()) && (is_null($ev["id"]) || (strlen($ev["id"]) == 0))))
					return $ev;
			}
		}
		return null;
	}

	public final function removeTriggerEventAt($pos)
	{
		if ((!$this->_trigger_events) || (!isset($this->_trigger_events[$pos])))
			return false;
		else
		{
			array_splice($this->_trigger_events, $pos, 1);
			return true;
		}
	}
	
	private final function decodeRequestEvents()
	{
		$events = json_decode($this->_request->post("__wb_events"), true);

		if ($events)
		{
			if (!$this->_trigger_events)
				$this->_trigger_events = array();
			foreach ($events as $ev)
			{
				if (!isset($ev["params"]))
					$ev["params"] = null;
				$this->_trigger_events[] = $ev;
			}
		}
	}
	
	/**
	 * Triggers the events that are specified in the request
	 * If $only_for_control is specified the events will be triggered 
	 * only for that control and it's children
	 *
	 * @param integer $trigger_time
	 * @param QWebControl $only_for_control
	 */
	public final function triggerRequestEvents($trigger_time = QWebEvent::TriggerAfterLoad, 
		QWebControl $only_for_control = null)
	{
		if ($this->_trigger_events)
		{
			$id_filter = null;
			if ($only_for_control)
			{
				$id_filter = $only_for_control->getFullId();
				$id_filter_len = strlen($id_filter);
			}
			
			foreach ($this->_trigger_events as $ev)
			{
				if (!isset($ev["params"]))
					$ev["params"] = null;
					
				if ($ev["id"])					
					$sender = $this->getElementById($ev["id"]);
				else 
					$sender = $this;
				if ($sender && (isset($ev["type"])) && ($ev["type"]))
				{
					if ($id_filter)
					{
						$full_id = $sender->getFullId();
						// if no match continue
						if (substr($full_id, 0, $id_filter_len) != $id_filter)
							continue;
						if ((strlen($full_id) > $id_filter_len) && (substr($full_id, $id_filter_len, strlen(self::IdSeparator)) != self::IdSeparator ))
							continue;
					}
					
					$triggered = $sender->triggerEvent($ev["type"], $ev["params"], true, $ev["id"], $trigger_time);
					
					if ($triggered)
					{
						if (!isset($this->_event_callback_js_data))
							$this->_event_callback_js_data = array();
						$this->_event_callback_js_data[] = array($ev["id"], $ev["type"], $ev["params"]);
					}
				}
				else 
				{
					// throw new Exception("The control `{$ev["id"]}` could not be found to trigger the event `{$ev["type"]}`");
				}
			}
		}
	}
	
	/**
	 * Gets the url manager associated with the module of the page
	 *
	 * @return QUrlManager
	 */
	public function getUrlManager()
	{
		if ($this->getModule())
			return $this->getModule()->getUrlManager();
		return null;
	}
	
	/**
	 * Return true if this control is a web page
	 *
	 * @return boolean
	 */
	public function isWebPage()
	{
		return true;
	}

	/**
	 * Returns true if this is an ajax request
	 *
	 * @return boolean
	 */
	public final function isAjaxRequest()
	{
		return $this->_is_ajax === true;
	}
	
	/**
	 * Appends a string to the javascript stream
	 *
	 * @param string $js
	 */
	public final function jsAppend($js)
	{
		$this->_js_stream .= $js;
	}
	
	/**
	 * Gets the base href of the page
	 *
	 * @return string
	 */
	public final function getBaseHref()
	{
		return $this->_base_href;
	}
	
	public final function getRequestUrl()
	{
		return $this->_request_url;
	}
	
	public final function getBaseHrefFull()
	{
		return $this->getUrlBaseHref();
	}
	
	public final function getUrlBaseHref()
	{
		$br = trim( $this->getBaseHref(), "/\\");
		return QWebContext::GetProtocol() . "://" . rtrim( QWebContext::GetHost() , "/\\") . "/" . 
			 $br . ((strlen($br) > 0) ? "/" : "");
	}
	
	public final function getUrlPathname()
	{
		return trim( $this->_base_href, "/\\") . "/";
	}
	
	/**
	 * Gets the id of the web page, the id is QWebPage for any web page
	 *
	 * @return string
	 */
	public final function getId()
	{
		return "QWebPage";
	}
	
	/**
	 * Setting the id of a web page it's forbidden, the id is it's class name
	 *
	 * @param string $value
	 */
	public function setId($value)
	{
		throw new Exception("You can not set the ID of a QWebPage, the id is by default it's name");
	}
	
	/**
	 * Gets the full id of the page
	 *
	 * @return string
	 */
	public final function getFullId()
	{
		return $this->getId();
	}
	
	/**
	 * Gets the instance id of the page. Based on the Id the page knows what viewstate to load.
	 *
	 * @return string
	 */
	public final function getInstanceId()
	{
		return $this->_instance_id;
	}
	
	/**
	 * Returns if this page has a postback
	 *
	 * @return boolean
	 */
	public final function isPostback()
	{
		return $this->_is_postback;
	}
	
	/**
	 * Gets the title of the web page
	 *
	 * @return string
	 */
	public function getTitle()
	{
		return $this->_title;
	}
	
	/**
	 * Sets the title of the page
	 *
	 * @param string $title
	 */
	public function setTitle($title)
	{
		$this->_title = $title;
		$old_title = $this->getViewstate("page_title");
		if ($old_title)
		{
			if ($old_title != $title)
				$this->setViewstate("new_page_title", $title);
		}
		else
			$this->setViewstate("new_page_title", $title);
	}
	
	/**
	 * Gets the description of the web page
	 *
	 * @return string
	 */
	public function getDescription()
	{
		return $this->_description;
	}
	
	/**
	 * Sets the description of the page
	 *
	 * @param string $description
	 */
	public function setDescription($description)
	{
		$this->_description = $description;
		$old_description = $this->getViewstate("page_description");
		if ($old_description)
		{
			if ($old_description != $description)
				$this->setViewstate("new_page_description", $description);
		}
		else
			$this->setViewstate("new_page_description", $description);
	}
	
	/**
	 * Gets the keywords of the web page
	 *
	 * @return string
	 */
	public function getKeywords()
	{
		return $this->_keywords;
	}
	
	/**
	 * Sets the keywords of the page
	 *
	 * @param string $keywords
	 */
	public function setKeywords($keywords)
	{
		$this->_keywords = $keywords;
		$old_keywords = $this->getViewstate("page_keywords");
		if ($old_keywords)
		{
			if ($old_keywords != $keywords)
				$this->setViewstate("new_page_keywords", $keywords);
		}
		else
			$this->setViewstate("new_page_keywords", $keywords);
	}
	
	/**
	 * Adds a chunk to the ajax chunks and returns the insert position
	 *
	 * @param mixed $type
	 * @param string $data
	 * @return integer The position where the chunk was inserted
	 */
	private final function addAjaxChunk($type, $data)
	{
		/* we make sure we do not insert on the same position twice ! */
		$pos = $this->_ajax_pos;
		$this->_ajax_chunks[$pos] = array($type, $data);
		$this->_ajax_pos++;
		return $pos;
	}
	
	/**
	 * Adds a replace chunk to the ajax chunks list
	 *
	 * @param string $element_id The id of the element to be replaced
	 * @param string $html_for_replace The html that will replace the element
	 * @return integer The position where the chunk was inserted
	 */
	public final function addAjaxReplaceChunk($element_id, $html_for_replace)
	{
		return $this->addAjaxChunk(QWebPage::AjaxChunkTypeReplace, array($element_id, $html_for_replace));
	}
	
	/**
	 * Adds a execute chunk to the ajax chunks list
	 *
	 * @param string $elemnt_id The id of the element to be replaced
	 * @param string $html_for_replace The html that will replace the element
	 * @return integer The position where the chunk was inserted
	 */
	public final function addAjaxExecuteChunk($js_as_text)
	{
		return $this->addAjaxChunk(QWebPage::AjaxChunkTypeExecute, $js_as_text);
	}
	
	/**
	 * Removes an ajax chunk from the specified position
	 *
	 * @param integer $pos The position of the chunk
	 * @return boolean True if an elemnent existed at that position
	 */
	public final function removeAjaxChunk($pos)
	{
		if (isset($this->_ajax_chunks[$pos]))
		{
			unset($this->_ajax_chunks[$pos]);
			return true;
		}
		else 
			return false;
	}
	
	/**
	 * Adds the resources of the control to the full list of 
	 * resources in this web page
	 *
	 * @param array $resources
	 */
	public final function addControlResources($resources)
	{
		if (!$resources)
			return;
		foreach ($resources as $path => $data)
			$this->_all_resources[$path] = $data;
	}
	
	public function outputDebugData()
	{
		if (QWebApp::InDebugMode())
		{
			$debug_div_style = "z-index: 1000; position: fixed; bottom: 0px; right: 0px; font-size: 11px; max-height:50%; max-width: 90%; overflow-y: scroll; overflow-x:auto; background-color: #fff6f6; border: 2px solid #4d4d4d;";
			$debug_div_style_ajax = $debug_div_style." display: none;";
			$debug_close_style = "position: absolute; background: url(\"".$this->res("debug_close.gif")."\") no-repeat; top: 3px; right: 3px; padding: 3px; width: 14px; height: 14px; cursor: pointer;";
			
			$debug = QWebApp::GetDebugData();
			
			if ($debug && (count($debug) > 0))
			{
				if ($this->isAjaxRequest())
				{
					$out = "";
					foreach ($debug as $line)
						$out .= "<div>{$line}</div><hr/>";
					
					$this->output( "GetQWebPage().setDebugText(". json_encode($out).");\n");
				}
				else 
				{
					$out = "<div id='QWebApp_DEBUG_DIV__' style='{$debug_div_style}'><div style='{$debug_close_style}' onclick='jQuery(this).parent().hide();'><!-- --></div><div id='QWebApp_DEBUG_DIV_TXT'>";
					foreach ($debug as $line)
						$out .= "<div>{$line}</div><hr/>";
					$out .= "</div></div>";
					
					$this->output($out);
				}
			}
			else
			{
				if (!$this->isAjaxRequest())
					$this->output("<div id='QWebApp_DEBUG_DIV__' style='{$debug_div_style_ajax}'><div style='{$debug_close_style}' onclick='jQuery(this).parent().hide();'><!-- --></div><div id='QWebApp_DEBUG_DIV_TXT'><!-- --></div></div>");
				else 
					// we hide it
					$this->output( "GetQWebPage().setDebugText(0);\n");
			}
		}
	}
	
	/**
	 * Gets the parent QIWebListener
	 *
	 * @return QIWebListener
	 */
	public function getParentWebListener()
	{
		return $this->_parent_web_listener;
	}
	
	/**
	 * Gets the call stack of the current QIWebListener as a string array
	 * 
	 * @param $skip_defaults If true will not put in the array the QIWebListener(s) that are the default listeners of their parent
	 *
	 * @return array
	 */
	public function getCallStack($skip_defaults = false)
	{
		$call_stack = array();
		$list = $this;
		
		while ($list != null)
		{
			$parent = $list->getParentWebListener();
			$class = $list->getClassName();
			
			if (!$parent)
			{
				// no need to include objects that are instance of QWebApp
				if (!($list instanceof QWebApp))
					$call_stack[] = $class;
				// end of line
				break;
			}
			
			if ((!$skip_defaults) || (!(($parent instanceof QWebModule) && ($parent->getDefaultListener() == $class))))
				$call_stack[] = $class;
			// else // has the same class as the default listener, no need to add it to the call stack
			
			$list = $parent;
		}
	}
	
	public function getEventCallbackJsData()
	{
		if (isset($this->_event_callback_js_data) && (count($this->_event_callback_js_data) > 0))
			return $this->_event_callback_js_data;
		else 
			return null;
	}
	
	public function setIncludeDefaultCSS($value)
	{
		$this->_include_default_css = $value;
	}
	
	public function getIncludeDefaultCSS()
	{
		return $this->_include_default_css;
	}
	/**
	 * Gets the page that is currently loaded
	 *
	 * @return QWebPage
	 */
	public static function GetLoadedPage()
	{
		return self::$_LoadedPage;
	}
	
	public function getSessionLoadTime()
	{
		return $this->_session_load_time;
	}
	
	public function setHasTriggeredEvents($value)
	{
		$this->_has_triggered_events = $value;
	}
	
	public function setDocType($doc_type)
	{
		$this->_doc_type = $doc_type;
	}
	
	public function getDocType()
	{
		return $this->_doc_type;
	}
	
	public function getHtmlExtra()
	{
		return $this->_html_extra;
	}
	
	public function setHtmlExtra($html_extra)
	{
		$this->_html_extra = " " . ltrim($html_extra);
	}
	
	public function getPageLink($page_class)
	{
		$stack = array();
		$stack[] = $page_class;
		
		$list = $this;
		
		while ($list = $list->getParentWebListener())
			$stack[] = $list->getClassName();
			
		$stack = array_reverse($stack);
		
		return QWebApp::GetCallStackLink($stack);
	}
	
	public function navigateTo($page_class)
	{
		// ?__qwl
		$url = $this->getBaseHrefFull().$this->getPageLink($page_class);
		
		if ($this->isAjaxRequest())
		{
			echo "document.location.href = ".json_encode($url).";";
			die();
		}
		else 
		{
			header("Location: ".$url);
			die();
		}
	}
	
	public function includeJQueryUI($themename = "smoothness", $version = "1.7.2")
	{
		$this->addCss(QWebApp::FwRel("res/misc/jquery-ui/css/{$themename}/jquery-ui-{$version}.custom.css"), null, false);
		$this->addJs(QWebApp::FwRel("res/misc/jquery-ui/js/jquery-ui-{$version}.custom.min.js"), null, false);
	}
	
	public function getIdWebListener()
	{
		return $this->getClassName();
	}
}

?>